package com.zmn.mcc.cas.server;

import com.alibaba.nacos.api.config.annotation.NacosValue;
import com.google.common.collect.Lists;
import com.zmn.common.constant.StatusConsts;
import com.zmn.common.dictionary.GlobalDict;
import com.zmn.common.dto2.ResponseDTO;
import com.zmn.common.utils.collection.CollectionUtil;
import com.zmn.common.utils.date.DateUtil;
import com.zmn.common.utils.number.NumberUtil;
import com.zmn.common.utils.string.StringUtil;
import com.zmn.mcc.business.interfaces.staff.dingtalk.DingTalkService;
import com.zmn.mcc.business.interfaces.staff.ip.StaffIpInfoBService;
import com.zmn.mcc.cas.CasConstants;
import com.zmn.mcc.cas.core.ShiroTokenManager;
import com.zmn.mcc.cas.model.LoginStaff;
import com.zmn.mcc.cas.model.UserTokenDO;
import com.zmn.mcc.cas.server.model.LoginDTO;
import com.zmn.mcc.cas.server.model.StaffLoginDIO;
import com.zmn.mcc.cas.web.StaffAdminService;
import com.zmn.mcc.common.constant.LoginTypeConsts;
import com.zmn.mcc.common.constant.RedisConsts;
import com.zmn.mcc.common.constant.StaffLogConsts;
import com.zmn.mcc.common.enums.DingTalkRecordEnum;
import com.zmn.mcc.model.entity.staff.McStaff;
import com.zmn.mcc.model.entity.staff.log.IpInfoVo;
import com.zmn.mcc.model.entity.staff.securitylog.StaffSecurityLog;
import com.zmn.mcc.services.interfaces.staff.securitylog.StaffSecurityLogService;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.session.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicInteger;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author quanjic
 * @version v1.0
 * @since 2018/12/3 22:53
 **/
@Service
public class StaffLoginService {

    private Logger logger = LoggerFactory.getLogger(StaffLoginService.class);

    @Resource
    CasServerStaffService casServerStaffService;

    @Resource
    ShiroTokenManager shiroTokenManager;

    @Resource
    private WhitelistService whitelistService;

    @Resource
    private RedisAtomicFactory redisAtomicFactory;

    @Resource
    private CaptchaService captchaService;

    @Resource
    private StaffSecurityLogService staffSecurityLogService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Resource
    StaffIpInfoBService staffIpInfoBService;

    @Resource
    DingTalkService dingTalkService;

    @NacosValue(value = "${mcc.staff.login.account.enabled:true}", autoRefreshed = true)
    private boolean enabled = true;

    /**
     * 输入3次错误后必须输入验证码
     */
    private Integer maxInputErrorMustVercode = 3;


    /**
     * 输入5次错误后禁止登录
     */
    private Integer maxInputError = 5;

    /**
     * 锁定时间：300秒
     */
    private Integer maxInputErrorLocketTimeSecond = 300;


    public Boolean isEnableAccountLogin() {
        return enabled;
    }

    /**
     * 登录
     *
     * @param dio        登录信息
     * @param requestIp  用户IP
     * @param vercodeKey 验证码KEY
     * @return
     */
    public LoginDTO login(StaffLoginDIO dio, String requestIp, String vercodeKey, Integer loginType) {

        if (dio == null) {
            return new LoginDTO(StatusConsts.STATUS_ERROR, dio.getRedirectUrl(), "ERROR", "请输入用户名与密码！");
        }
        if (StringUtils.isBlank(dio.getStaffName())) {
            return new LoginDTO(StatusConsts.STATUS_ERROR, dio.getRedirectUrl(), "ERROR", "请输入用户名！");
        }
        if (StringUtil.isBlank(dio.getPassword())) {
            return new LoginDTO(StatusConsts.STATUS_ERROR, dio.getRedirectUrl(), "ERROR", "请输入密码！");
        }

        //输错密码计数器
        RedisAtomicInteger errorCounter = null;

        McStaff user = null;
        try {

            logger.info("#cas#login requestIp:{}, vercodeKey:{}, user:{}", requestIp, vercodeKey, dio);

            user = casServerStaffService.queryStaff(dio.getStaffName());

            if (user == null) {
                return new LoginDTO(StatusConsts.ERROR_NOTUSER, dio.getRedirectUrl(), "ACCOUNT_E", "账号不存在！");
            }

            //IP白名单验证
            if (whitelistService.isEnabled() && !whitelistService.canLogin(user, requestIp)) {
                logger.warn("#cas#login 非白名单用户，禁止登录；, requestIp:{}, vercodeKey:{}, user:{}", requestIp, vercodeKey, dio);
                return new LoginDTO(StatusConsts.ERROR_ADMIN_LOGIN_NO, null, "NO_PERMIT", "非公司内部网络，禁止登录系统");
            }

            // 校验状态
            if (user.getStatus() != GlobalDict.STATUS_ENABLE) {
                return new LoginDTO(StatusConsts.FAIL_LOGIN_DISABLE, dio.getRedirectUrl(), "ERROR", "账号已被禁用或锁定");
            }

            //检验密码错误次数
            errorCounter = getInputPasswordErrorCounter(user.getStaffId());
            int errorCount = errorCounter.get();
            if (errorCount >= maxInputError) {
                return new LoginDTO(StatusConsts.FAIL_LOGIN_TOO_MANY_TIMES, dio.getRedirectUrl(), "ERROR", String.format("密码错误次数已超过%s次，请 %s 分钟以后重试", maxInputError, maxInputErrorLocketTimeSecond / 60));
            }
            /*else if (errorCount >= maxInputErrorMustVercode) {
                return new LoginDTO(StatusConsts.FAIL_LOGIN_PASSWORD, dio.getRedirectUrl(), "PWD_E_CODE", "验证码错误！");
                //return new LoginDTO(StatusConsts.STATUS_ERROR, dio.getRedirectUrl(), "ERROR", "请输入验证码");
            }*/

            // 验证码
            if (errorCount >= maxInputErrorMustVercode) {
                if (StringUtils.isBlank(vercodeKey)) {
                    return new LoginDTO(StatusConsts.STATUS_ERROR, dio.getRedirectUrl(), "PWD_E_CODE", "验证码Key丢失，请稍后重试");
                }
                if (StringUtils.isBlank(dio.getVercode())) {
                    return new LoginDTO(StatusConsts.FAIL_LOGIN_PASSWORD, dio.getRedirectUrl(), "PWD_E_CODE", "请输入验证码");
                }
                if (!captchaService.check(vercodeKey, dio.getVercode())) {
                    return new LoginDTO(StatusConsts.FAIL_LOGIN_PASSWORD, dio.getRedirectUrl(), "PWD_E_CODE", "验证码错误！");
                }
            }

            // 判斷是否已登录
            if (NumberUtil.isPositiveInteger(user.getStaffId())) {
                Object sessionOld = redisTemplate.opsForHash().get(RedisConsts.CAS_STAFF_SESSION, user.getStaffId().toString());
                if (Objects.nonNull(sessionOld)) {
                    redisTemplate.opsForHash().delete(RedisConsts.CAS_STAFF_SESSION, user.getStaffId().toString());
                    redisTemplate.opsForZSet().remove(CasConstants.ONLINE_STAFF_KEY, sessionOld.toString());
                    // 写入登录日志
                    insertSecurityLog(user, StaffLogConsts.SECURITY_LOG_MULTIPLE_LOGIN, requestIp);
                    kickout(sessionOld.toString());
                    logger.info("#cas#login 已在其它地方登录，踢出之前登录；, requestIp:{}, sessionOld:{}, user:{}", requestIp, sessionOld, dio);
                }

                //判断是否异地登录
                Object oldIp = redisTemplate.opsForHash().get(RedisConsts.CAS_STAFF_IP, user.getStaffId().toString());
                if (Objects.nonNull(oldIp)) {
                    checkIp(oldIp.toString(), requestIp, user.getDTalkId());
                    redisTemplate.opsForHash().delete(RedisConsts.CAS_STAFF_IP, user.getStaffId().toString());
                }
            }

            //判断是否是所在工作城市登录
//            String city = getIpCity(requestIp);
//            if (StringUtils.isNotEmpty(city) && !city.contains(user.getWorkPlaceCity())) {
//                return new LoginDTO(StatusConsts.STATUS_ERROR, dio.getRedirectUrl(), "WORK_PLACE_E_CODE", "当前城市非工作地点，禁止登录");
//            }

            // 绑定钉钉userId
            if (StringUtils.isNotEmpty(dio.getDTalkUserId())) {
                Boolean flag = bindDTalkUserId(dio.getDTalkUserId(), user);
                if (!flag) {
                    return new LoginDTO(StatusConsts.STATUS_ERROR, dio.getRedirectUrl(), "ERROR", "该账号已绑定过钉钉，请更换后再试");
                }
            }

            // 登录
            UserTokenDO userTokenDO;
            if (LoginTypeConsts.LOGIN_TYPE_ACCOUNT.equals(loginType)) {
                userTokenDO = shiroTokenManager.login(null, dio.getStaffName(), dio.getPassword(), false);
            } else {
                userTokenDO = shiroTokenManager.loginByQr(null, dio.getStaffName(), dio.getDTalkUserId());
            }

            redisTemplate.opsForHash().put(RedisConsts.CAS_STAFF_SESSION, user.getStaffId().toString(), userTokenDO.getSessionId());
            redisTemplate.opsForHash().put(RedisConsts.CAS_STAFF_IP, user.getStaffId().toString(), requestIp);

            dio.setOperatorId(user.getStaffId());
            dio.setOperatorIP(requestIp);
            try {
                // 清楚密码输入错误次数
                errorCounter.expire(0, TimeUnit.MILLISECONDS);
                // 更新最后一次登录时间
                casServerStaffService.updateLastLoginTime(user.getStaffId());
                // 写入登录日志
                insertSecurityLog(user, StaffLogConsts.SECURITY_LOG_LOGIN, requestIp);
//                casServerStaffService.saveLoginLog(CasServerStaffService.LOGIN_TYPE_IN, CasServerStaffService.LOGIN_STATUS_SUCCESS, StatusConsts.STATUS_SUCCESS, "");
            } catch (Exception e) {
                logger.error("#cas#login login，禁止登录；, requestIp:{}, vercodeKey:{}, user:{}", requestIp, vercodeKey, dio, e);
            }

            return new LoginDTO(StatusConsts.STATUS_SUCCESS, dio.getRedirectUrl(), "OK", "登录成功！");

        } catch (AuthenticationException ex) {
            //连续三次输错密码，那么提示出现验证码框
            int currentCount = 0;
            if (StringUtils.isEmpty(dio.getDTalkUserId())) {
                currentCount = errorCounter.incrementAndGet();
            }
            logger.info("#cas#login 身份认证不通过; requestIp:{}, vercodeKey:{}, user:{}, errorCount:{}, message:{}", requestIp, vercodeKey, dio, currentCount, ex.getMessage());
            errorCounter.expire(maxInputErrorLocketTimeSecond, TimeUnit.SECONDS);

            if (currentCount >= maxInputError) {
                return new LoginDTO(StatusConsts.FAIL_LOGIN_TOO_MANY_TIMES, dio.getRedirectUrl(), "ERROR", String.format("密码错误次数已超过%s次，请 %s 分钟以后重试", maxInputError, maxInputErrorLocketTimeSecond / 60));
            } else if (currentCount >= maxInputErrorMustVercode) {
                String statusCode = "PWD_E" + currentCount;
                return new LoginDTO(StatusConsts.FAIL_LOGIN_PASSWORD, dio.getRedirectUrl(), statusCode, "密码错误！");
            } else {
                return new LoginDTO(StatusConsts.FAIL_LOGIN_PASSWORD, dio.getRedirectUrl(), null, "密码错误！");
            }
        } catch (Exception ex) {
            logger.error("#cas#login 登录失败; requestIp:{}, vercodeKey:{}, user:{}", requestIp, vercodeKey, dio, ex);
            String message = StringUtils.substring(ex.getMessage(), 0, 200);
            // 写入登录日志
//            casServerStaffService.saveLoginLog(CasServerStaffService.LOGIN_TYPE_IN, CasServerStaffService.LOGIN_STATUS_Fail, StatusConsts.STATUS_ERROR, message);
            return new LoginDTO(StatusConsts.STATUS_ERROR, dio.getRedirectUrl(), "ERROR", message);
        }
    }

    /**
     * 登出
     *
     * @return
     */
    public Boolean logout(String requestIp) {
        try {
            if (!SecurityUtils.getSubject().isAuthenticated()) {
                return false;
            }

            LoginStaff staff = StaffAdminService.currentStaff();

            logger.info("#cas#logout 登出；staff:{}", staff);

            // 删除登录sessionID缓存
            redisTemplate.opsForHash().delete(RedisConsts.CAS_STAFF_SESSION, staff.getStaffId().toString());

            // 登出
            shiroTokenManager.logout();
            // 写入登出日志
            StaffSecurityLog log = new StaffSecurityLog();
            log.setStaffId(staff.getStaffId());
            log.setCreater(staff.getRealName());
            log.setLogType(StaffLogConsts.SECURITY_LOG_LOGOUT);
            log.setOpertorIp(requestIp);
            log.setCreateTime(new Date());
            staffSecurityLogService.insert(log);
//            casServerStaffService.saveLoginLog(CasServerStaffService.LOGIN_TYPE_OUT, CasServerStaffService.LOGIN_STATUS_SUCCESS, StatusConsts.STATUS_SUCCESS, "");
            return true;
        } catch (Exception ex) {
            logger.error("#cas#logout 登出失败", ex);
            return false;
        }
    }

    /**
     * 根据临时授权码获取userId
     * @param authCode
     * @return
     */
    public String getDingTalkUserId(String authCode) {
        logger.info("临时授权码 authCode = {}", authCode);
        ResponseDTO<String> result = dingTalkService.getUserIdByAuthCode(authCode);
        if (result.isSuccess()) {
            return result.getData();
        }
        logger.error("使用临时授权码获取用户信息结果失败 result = {}", result);
        return null;
    }

    /**
     * 踢出登录
     *
     * @param sessionId 要踢出的用户SessionId
     * @return
     */
    public Boolean kickout(String sessionId) {
        try {
            Session session = shiroTokenManager.getSession(sessionId);
            UserTokenDO token = shiroTokenManager.getUserToken(session);

            if (!token.getSessionStatus()) {
                return true;
            }


            // 踢出
            shiroTokenManager.kickout(sessionId);
            // 写入踢出日志 TODO 暂未使用
//            loginLogStaffBService.saveLoginLog(session, token.getUserKey(), CasServerStaffService.LOGIN_TYPE_KICK, GlobalDict.RESULT_SUCCESS, StatusConsts.STATUS_SUCCESS, "", shiroTokenManager.getUserToken());
            return true;
        } catch (Exception ex) {
            logger.error("#cas#kickout 踢出失败； sessionId:{}", sessionId);
            // 写入踢出日志
//            loginLogStaffBService.saveLoginLog(session, token.getUserKey(), UserDict.LOGIN_TYPE_KICK, GlobalDict.RESULT_FAIL, StatusConsts.STATUS_ERROR, "", shiroTokenManager.getUserToken());
            return false;
        }
    }

    private RedisAtomicInteger getInputPasswordErrorCounter(Integer staffId) {

        return redisAtomicFactory.create(CasServerConsts.CAS_PWD_ERROR_COUNTER_KEY + staffId);
    }

    /**
     * 检查ip是否异地
     *
     * @param oldIp
     * @param requestIp
     */
    private void checkIp(String oldIp, String requestIp, String userId) throws Exception {
        logger.info("上一次登录ip为: {}, 本次登录ip为: {}", oldIp, requestIp);
        if (StringUtils.isEmpty(oldIp) || StringUtils.isEmpty(requestIp)) {
            return;
        }
        List<String> ipList = Lists.newArrayList(oldIp, requestIp);
        List<IpInfoVo> ipInfoVoList = staffIpInfoBService.listIpInfo(ipList);
        logger.info("查询到ip归属地信息 : {}" + ipInfoVoList);
        if (CollectionUtil.isNullOrEmpty(ipInfoVoList)) {
            return;
        }

        Set<String> cityList = ipInfoVoList.stream().map(IpInfoVo::getCity).collect(Collectors.toSet());
        //ip异地登录异常则发送钉钉消息
        if (cityList.size() > 1 && StringUtils.isNotEmpty(userId)) {
            IpInfoVo ipInfoVo = ipInfoVoList.stream().filter(e -> requestIp.equals(e.getIp())).findAny().orElse(null);
            if (Objects.nonNull(ipInfoVo)) {
                //拼接消息
                StringBuilder sb = new StringBuilder();
                sb.append(DingTalkRecordEnum.IP_ABNORMAL.getTitle()).append("您的运营平台系统账号于")
                        .append(DateUtil.getNowFormatted()).append("在")
                        .append(ipInfoVo.getProvince()).append(ipInfoVo.getCity()).append("（IP:")
                        .append(requestIp).append("）登录，如非本人操作，请尽快登录修改密码。");
                logger.info("准备发送钉钉消息， userId:{}, content:{}", userId, sb.toString());
                dingTalkService.sendDingTalkWorkRecord(userId,
                        sb.toString());
            }
        }
    }

    /**
     * 获取当前登录所在城市
     *
     * @param requestIp
     * @return
     */
    private String getIpCity(String requestIp) {
        List<String> ipList = Lists.newArrayList(requestIp);
        List<IpInfoVo> ipInfoList = staffIpInfoBService.listIpInfo(ipList);
        if (CollectionUtil.isNullOrEmpty(ipInfoList)) {
            return null;
        }
        return ipInfoList.get(0).getCity();
    }

    /**
     * 钉钉userId绑定员工账号
     *
     * @param dTalkUserId
     * @param user
     */
    private Boolean bindDTalkUserId(String dTalkUserId, McStaff user) {
        if (StringUtils.isNotEmpty(user.getDTalkId())) {
            if (!dTalkUserId.equals(user.getDTalkId())) {
                return false;
            }
            return true;
        }
        user.setDTalkId(dTalkUserId);
        casServerStaffService.updateDTalkId(user);
        return true;
    }

    /**
     * 新增安全日志
     */
    private void insertSecurityLog(McStaff staff, Integer operateType, String ip) {
        StaffSecurityLog log = new StaffSecurityLog();
        log.setStaffId(staff.getStaffId());
        log.setCreater(staff.getRealName());
        log.setLogType(operateType);
        log.setOpertorIp(ip);
        log.setCreateTime(new Date());
        staffSecurityLogService.insert(log);
    }
}
